Linux 中传统 IO 模式
基础概念题
1. 请解释什么是 IO 模式,以及一次完整的 IO 操作包含哪两个阶段?
参考答案: IO 模式是操作系统处理输入输出操作的不同方式。一次完整的 IO 操作(以 read 为例)包含两个阶段:
- 等待数据准备阶段:数据从网络/磁盘等拷贝到操作系统内核的缓冲区
- 数据拷贝阶段:将数据从内核缓冲区拷贝到应用程序的地址空间
追问:为什么需要这两个阶段?
- 内核缓冲区起到缓存作用,提高 IO 效率
- 保护模式下,用户态程序不能直接访问硬件资源
- 内核统一管理 IO 资源,确保系统稳定性
2. Linux 系统中有哪五种主要的 IO 模式?在 Go 开发中你主要接触过哪些?
参考答案: 五种 IO 模式:
- 阻塞 I/O(Blocking IO)
- 非阻塞 I/O(Non-blocking IO)
- I/O 多路复用(IO Multiplexing)
- 信号驱动 I/O(Signal-driven IO) - 基本不用
- 异步 I/O(Asynchronous IO)
在 Go 开发中主要接触:
- IO 多路复用:Go 的 netpoller 基于 epoll/kqueue 实现
- 阻塞 IO:Go 的 goroutine 从用户角度看是阻塞的,但底层是非阻塞的
阻塞 IO 深度解析
3. 请描述阻塞 IO 的工作流程,并分析其优缺点
工作流程:
特点:
- 在 IO 执行的两个阶段都被阻塞
- 进程会一直等待直到操作完成
优缺点分析:
| 优点 | 缺点 |
|---|---|
| 编程模型简单 | 并发性差 |
| 资源利用率在单连接下较高 | 需要多线程处理多连接 |
| 系统调用次数少 | 线程切换开销大 |
面试追问:在 Go 中如何实现看似阻塞但实际高效的 IO?
4. 为什么说阻塞 IO 不适合高并发场景?请举例说明
参考答案: 在高并发场景下,阻塞 IO 的问题:
- C10K 问题:每个连接需要一个线程,1万个连接需要1万个线程
- 内存消耗:每个线程栈空间通常 8MB,1万线程需要 80GB 内存
- 上下文切换开销:大量线程切换消耗 CPU 资源
- 线程创建销毁成本:频繁创建销毁线程影响性能
// 传统阻塞 IO 处理多连接的方式
func handleConnection(conn net.Conn) {
defer conn.Close()
// 阻塞读取数据
buffer := make([]byte, 1024)
n, err := conn.Read(buffer) // 这里会阻塞
// 处理数据...
}
func main() {
listener, _ := net.Listen("tcp", ":8080")
for {
conn, _ := listener.Accept()
go handleConnection(conn) // 每个连接一个 goroutine
}
}
非阻塞 IO 深度解析
5. 非阻塞 IO 如何解决阻塞 IO 的问题?它又带来了什么新问题?
工作流程:
解决的问题:
- 避免进程/线程阻塞
- 单线程可以处理多个连接
- 提高 CPU 利用率
带来的新问题:
- CPU 密集轮询:需要不断调用系统调用检查状态
- 系统调用开销:频繁的用户态/内核态切换
- 编程复杂度:需要手动管理状态机
面试追问:Go 语言是如何避免这些问题的?